/*
 * Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/*
 * qmiwds: QMI calls on the WDS (Wireless Data Service) interface
 *
 * Sources used in writing this file (see README for links):
 * [GobiNet]/QMI.c
 * [GobiNet]/QMIDevice.c
 * [cros-kerne]/drivers/net/usb/gobi/qmi.c
 * [cros-kerne]/drivers/net/usb/gobi/qmidevice.c
 */

#include <assert.h>
#include <errno.h>
#include <glib.h>

#include <libqmi.h>

#include "error.h"
#include "qmi.h"
#include "qmidev.h"
#include "qmimsg.h"

struct qmiwds_priv {
  struct listener *packet_status_listener;
  qmidev_packet_status_callback_fn packet_status_callback;
  void *packet_status_context;
};

static int packet_status_changed(struct qmidev *qmidev,
                                 void *data,
                                 struct qmimsg *message)
{
  struct client *client = data;
  struct qmiwds_priv *priv = client_priv(client);
  uint16_t state = 0xfe;
  int result;

  /* unused */
  qmidev = qmidev;
  client = client;

  result = qmimsg_tlv_get(message, QMI_TLV_VALUE, sizeof(state), &state);

  if (priv->packet_status_callback)
    priv->packet_status_callback(qmidev,
                                 priv->packet_status_context,
                                 result,
                                 state);

  return 0;
}

static void client_create(struct qmidev *qmidev, struct client *client)
{
  struct qmiwds_priv *priv;
  struct listen_criteria criteria;

  criteria.flags = LISTEN_MATCH_QMI_FLAGS |
                   LISTEN_MATCH_MESSAGE;
  criteria.qmi_flags = QMI_FLAG_SVC_INDICATION;
  criteria.message = QMIWDS_MSG_PACKET_STATUS;

  priv = g_slice_new(struct qmiwds_priv);
  priv->packet_status_listener = qmidev_listen(qmidev,
                                               client,
                                               &criteria,
                                               &packet_status_changed,
                                               client);

  priv->packet_status_callback = NULL;
  priv->packet_status_context = NULL;
  client_set_priv(client, priv);

  /* unused */
  qmidev = qmidev;
}

static void client_destroy(struct qmidev *qmidev, struct client *client)
{
  struct qmiwds_priv *priv = client_priv(client);
  g_slice_free(struct qmiwds_priv, priv);

  /* unused */
  qmidev = qmidev;
}

struct service qmiwds_service = {
  .service_id   = QMI_SVC_WDS,
  .event_msg_id = QMIWDS_MSG_EVENT,
  .create_fn    = &client_create,
  .destroy_fn   = &client_destroy
};

struct get_packet_status_context {
  qmidev_packet_status_response_fn callback;
  void *context;
};

static void get_packet_status_response(struct qmidev *qmidev,
                                       void *data,
                                       int result,
                                       struct qmimsg *message)
{
  struct get_packet_status_context *context = data;
  uint32_t state = 0;

  assert(qmidev);
  assert(data);

  if (result)
    goto out;

  result = qmimsg_tlv_get(message, QMI_TLV_VALUE, sizeof(state), &state);

out:
  context->callback(qmidev, context->context, result, state);
  g_slice_free(struct get_packet_status_context, context);
}

int qmidev_get_packet_status(struct qmidev *qmidev,
                             qmidev_packet_status_response_fn caller_callback,
                             void *caller_context)
{
  struct get_packet_status_context *context;
  int result;

  context = g_slice_new(struct get_packet_status_context);
  context->callback = caller_callback;
  context->context  = caller_context;

  result = qmidev_request(qmidev, &qmiwds_service,
                          QMIWDS_MSG_PACKET_STATUS,
                          NULL, NULL,
                          &get_packet_status_response, context);
  if (result)
    g_slice_free(struct get_packet_status_context, context);
  return result;
}

int qmidev_set_packet_status_callback(
    struct qmidev *qmidev,
    qmidev_packet_status_callback_fn callback,
    void *context,
    qmidev_response_fn set_callback,
    void *set_context)
{
  struct client *client;
  struct qmiwds_priv *priv;

  client = qmidev_default_client(qmidev, &qmiwds_service);
  priv = client_priv(client);
  priv->packet_status_callback = callback;
  priv->packet_status_context = context;
  /* TODO: Call later. */
  if (set_callback)
    set_callback(qmidev, set_context, 0);
  return 0;
}

struct start_network_context {
  qmidev_start_network_response_fn callback;
  void *context;
};

enum {
  QMIWDS_TLV_START_NETWORK_CALL_END_REASON = 0x10,
  QMIWDS_TLV_START_NETWORK_VERBOSE_CALL_END_REASON = 0x11
};

static void start_network_response(struct qmidev *qmidev,
                                   void *data,
                                   int result,
                                   struct qmimsg *message)
{
  struct start_network_context *context = data;
  /* Arbitrary recognizable values, to debug whether the TLV is present. */
  qmidev_session_id session_id = 0xfefefefe;
  uint16_t call_end_type = 0xfefe;

  qmimsg_tlv_get(message, QMI_TLV_VALUE, sizeof(session_id), &session_id);
  qmimsg_tlv_get(message, QMIWDS_TLV_START_NETWORK_CALL_END_REASON,
                 sizeof(call_end_type), &call_end_type);

  /* TODO: Verbose call end reason? */

  context->callback(qmidev, context->context, result,
                    session_id, call_end_type);
  g_slice_free(struct start_network_context, context);
}

int qmidev_start_network(struct qmidev *qmidev,
                         qmidev_start_network_response_fn caller_callback,
                         void *caller_context)
{
  struct start_network_context *context;
  int result;

  context = g_slice_new(struct start_network_context);
  context->callback = caller_callback;
  context->context = caller_context;

  result = qmidev_request(qmidev, &qmiwds_service,
                          QMIWDS_MSG_START_NETWORK,
                          NULL, NULL,
                          &start_network_response, context);
  if (result)
    g_slice_free(struct start_network_context, context);
  return result;
}

static int stop_network_request(void *data,
                                struct qmimsg *message)
{
  qmidev_session_id *session_id = data;
  return qmimsg_tlv_add(message,
                        QMI_TLV_VALUE,
                        sizeof(*session_id),
                        session_id);
}

int qmidev_stop_network(struct qmidev *qmidev,
                        qmidev_session_id session_id,
                        qmidev_response_fn caller_callback,
                        void *caller_context)
{
  return qmidev_void_request(qmidev, &qmiwds_service,
                             QMIWDS_MSG_STOP_NETWORK,
                             &stop_network_request, &session_id,
                             caller_callback, caller_context);
}
